package com.aphrodite.xml;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Stack;

/**
 * Extremely light weight XML reader
 * 
 * @author kentelt
 * 
 */
public class XMLReader {

	public final static byte END_DOCUMENT = 1;
	public final static byte END_TAG = 3;
	public final static byte START_DOCUMENT = 0;
	public final static byte START_TAG = 2;
	public final static byte TEXT = 4;
	private final Hashtable attributes = new Hashtable();
	private int c;
	private boolean inside_tag;
	private final InputStream is;
	private String tagName;
	private Stack tags;
	private String text;
	private byte type = START_DOCUMENT;

	public XMLReader(final InputStream in) throws IOException {
		this.is = in;
		this.tags = new Stack();
		this.inside_tag = false;
	}

	public void close() throws IOException {
		is.close();
	}

	/**
	 * Get the value for the given attribute
	 * 
	 * @param name
	 *            of the attribute
	 * @return attribute value
	 */
	public String getAttribute(final String name) {
		return (String) this.attributes.get(name);
	}

	/**
	 * Get the number of attributes in the given tag
	 * 
	 * @return
	 */
	public int getAttributeCount() {
		return attributes.size();
	}

	/**
	 * Get the attributes for a given tag
	 * 
	 * @return The list of attributes for a given tag
	 */
	public Enumeration getAttributes() {
		return this.attributes.keys();
	}

	/**
	 * Returns the current tag name
	 * 
	 * @return The current tag name
	 */
	public String getName() {
		return this.tagName;
	}

	private int getNextCharacter() throws IOException {
		int a = is.read();
		int t = a;

		if ((t | 0xC0) == t) {
			int b = is.read();
			if (b == 0xFF) { // Check if legal
				t = -1;
				// } else if (b < 0x80) { // Check for UTF8 compliancy
				// throw new IOException("Bad UTF-8 Encoding encountered");
			} else if ((t | 0xE0) == t) {
				int c = is.read();
				if (c == 0xFF) { // Check if legal
					t = -1;
					// } else if (c < 0x80) { // Check for UTF8 compliancy
					// throw new IOException("Bad UTF-8 Encoding encountered");
				} else {
					t = ((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F);
				}
			} else {
				t = ((a & 0x1F) << 6) | (b & 0x3F);
			}
		}
		return t;
	}

	public String getText() {
		return this.text;
	}

	public int getType() {
		return this.type;
	}

	/**
	 * read the next tag and return its type
	 * 
	 * @return the type of the tag read
	 * @throws IOException
	 */
	public int next() throws IOException {
		/*
		 * while (!this.ready()) try { java.lang.Thread.sleep(100); } catch
		 * (InterruptedException e) {}
		 */
		this.c = getNextCharacter();
		if (this.c <= ' ') {
			while (((this.c = getNextCharacter()) <= ' ') && (this.c != -1)) {
				;
			}
		}
		if (this.c == -1) {
			this.type = END_DOCUMENT;
			return this.type;
		}

		if ((this.c == '<') || ((this.c == '/') && !this.inside_tag)) {
			this.inside_tag = true;
			// reset all
			this.tagName = null;
			this.text = null;
			this.attributes.clear();

			if (this.c == '<') {
				this.c = getNextCharacter();
			}
			if (this.c == '/') {
				this.type = END_TAG;
				this.c = getNextCharacter();
				this.tagName = this.readName('>');
			} else if ((this.c == '?') || (this.c == '!')) {// ignore xml
				// heading & //
				// comments
				while ((this.c = getNextCharacter()) != '>') {
					;
				}
				this.next();
			} else {
				this.type = START_TAG;
				this.tagName = this.readName(' ');

				String attribute = "";
				String value = "";
				while (this.c == ' ') {
					this.c = getNextCharacter();
					attribute = this.readName('=');

					int quote = getNextCharacter();// this.c = this.read(); //
					// '''
					this.c = getNextCharacter();
					value = this.readText(quote); // change from value =
					// this.readText(''');
					this.c = getNextCharacter();
					this.attributes.put(attribute, value);
				}
				if (this.c != '/') {
					this.inside_tag = false;
				}
			}
		} else if ((this.c == '>') && this.inside_tag) // last tag ended
		{
			this.type = END_TAG;
			this.inside_tag = false;
		} else {
			this.tagName = null;
			this.attributes.clear();

			this.type = TEXT;
			this.text = this.readText('<');
		}

		return this.type;
	}

	private String readName(final int end) throws IOException {
		final StringBuffer output = new StringBuffer("");
		do {
			output.append((char) this.c);
		} while (((this.c = getNextCharacter()) != end) && (this.c != '>') && (this.c != '/'));
		return output.toString();
	}

	/**
	 * Get the text between a tag definition
	 * 
	 * @return
	 * @throws IOException
	 */
	public String readText() throws IOException {
		final String endTagName = getName();
		final StringBuffer str = new StringBuffer("");
		int t = next(); // omit start tag
		while (!endTagName.equals(getName())) {
			if (t == XMLReader.TEXT) {
				str.append(getText());
			}
			t = next();
		}
		return str.toString();
	}

	private String readText(final int end) throws IOException {
		final StringBuffer output = new StringBuffer("");
		while (this.c != end) {
			if (this.c == '&') {
				this.c = getNextCharacter();
				switch (this.c) {
				case 'l':
					output.append('<');
					break;
				case 'g':
					output.append('>');
					break;
				case 'a':
					if (getNextCharacter() == 'm') {
						output.append('&');
					} else {
						output.append('\'');
					}
					break;
				case 'q':
					output.append('"');
					break;
				case 'n':
					output.append(' ');
					break;
				default:
					output.append('?');
				}

				while ((this.c = getNextCharacter()) != ';') {
					;
				}
			} else if (this.c == '\\') {
				if ((this.c = getNextCharacter()) == '<') {
					break;
				} else {
					output.append((char) this.c);
				}
			} else {
				output.append((char) this.c);
			}
			this.c = getNextCharacter();
		}
		// while((c = read()) != end);
		return output.toString();
	}

	public void skip() throws IOException {
		int x;
		while ((x = next()) != XMLReader.END_TAG) {
			if (x == XMLReader.START_TAG) {
				skip();
			}
		}
	}
};
